package org.voidness.oje2d;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;

import javax.imageio.ImageIO;

import org.lwjgl.BufferUtils;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.OpenGLException;
import org.voidness.oje2d.gui.GuiEngine;
import org.voidness.oje2d.interfaces.EngineInterface;

/** The main Freyr2D engine. */
public class Engine {

    /** Singleton instance of the engine */
    public static Engine instance = null;
    /** The connection with the game implementation */
    private EngineInterface child = null;
    /** The window's width value */
    private int width;
    /** The window's height value */
    private int height;
    /** The window's depth value */
    private int depth;
    /** Whether the engine is in fullscreen or not */
    private boolean isFullscreen;
    /** Has quit been requested? */
    private boolean quitRequested;
    /** Frames per second counter */
    private int fps;
    /** Average frames per second */
    private int lastFps;
    /** Last check of the FPS value */
    private long lastTime;
    /** The current time of execution */
    private long currentTime;
    /** The current cursor */
    private GLImage cursor = null;
    /** Current mouse position */
    private Vector2D currentMousePosition;
    /** Last mouse position */
    private Vector2D lastMousePosition;
    /** Is the control key beeing held down? */
    private boolean controlKey;
    /** Is the alt key beeing held down? */
    private boolean altKey;
    /** Is the shift key beeing held down? */
    private boolean shiftKey;
    private boolean[] keys = null;
    private int lastImageId;
    private Vector2D lastImagePos = null;
    private Vector2D lastImageSize = null;

    public void setLastImageUnderMouse(int mId, Vector2D mPos, Vector2D mSize) {

        lastImageId = mId;
        lastImagePos = mPos;
        lastImageSize = mSize;
    }

    public int getImageUnderMouseId() {

        return lastImageId;
    }

    /**
     * Returns the engine's single instance.
     * 
     * @return The engine's instance
     */
    public static Engine getInstance() {

        if (instance == null) {

            instance = new Engine();
        }
        return instance;
    }

    /**
     * Private constructor for the engine.
     */
    private Engine() {

        lastTime = Sys.getTime();// System.currentTimeMillis();
        isFullscreen = false;
        quitRequested = false;

        controlKey = false;
        altKey = false;
        shiftKey = false;

        keys = new boolean[512];
    }

    /**
     * Engine initialization method.
     * 
     * @param mWidth Width of the display
     * @param mHeight Height of the display
     * @param mDepth Bit depth of the display
     * @param mFullscreen True for fullscreen mode
     * @param mChild The child engine calling init
     * @return True if succeded, false if not
     */
    public boolean init(int mWidth, int mHeight, int mDepth,
            boolean mFullscreen, EngineInterface mChild) {

        Utils.dump("Engine started.", Utils.INFORMATION);

        // Define the window's properties
        width = mWidth;
        height = mHeight;
        depth = mDepth;

        // Show the image formats supported
        String formatsMessage = "Image formats supported: ";
        String[] formats = ImageIO.getReaderFormatNames();
        for (int i = 0; i < formats.length; i++) {

            formatsMessage += formats[i] + " ";
        }
        Utils.dump(formatsMessage, Utils.INFORMATION);

        // Define the child who inited the engine
        child = mChild;

        // If in fullscreen mode change the display
        if (mFullscreen) {

            setFullscreen(true);
            Utils.dump("Fullscreen mode set.", Utils.INFORMATION);

        } else {

            setFullscreen(false);
            Utils.dump("Window mode set.", Utils.INFORMATION);
        }

        // Initialize OpenGL parameters
        initGL(800, 600);
        Utils.dump("OpenGL initialized.", Utils.INFORMATION);

        // Check for hardware mouse support
        // if (Mouse.getNativeCursor() == null) {
        // Utils.dump("No HW cursor support!", Utils.WARNING);
        // System.exit(0);
        // } else {
        // Grab the mouse
        Mouse.setGrabbed(true);
        if ((new File("resources/cursors/bigCursor.png")).exists()) {

            cursor = new GLImage("resources/cursors/bigCursor.png");
        }
        // }

        // Let the main program initialize
        child.init();
        Utils.dump("Main program initialized.", Utils.INFORMATION);

        currentMousePosition =
                new Vector2D(mWidth / 2, Display.getDisplayMode().getHeight() - mHeight / 2);
        lastMousePosition =
                new Vector2D(mWidth / 2, Display.getDisplayMode().getHeight() - mHeight / 2);

        return true;
    }

    /**
     * Private OpenGL initialization method.
     * 
     * @param mWidth The display's width
     * @param mHeight The display's height
     */
    private void initGL(int mWidth, int mHeight) {

        // View aspects
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glLoadIdentity();
        GL11.glOrtho(0, mWidth, 0, mHeight, -1, 1);
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
   		GL11.glLoadIdentity();
   		GL11.glViewport(0, 0, mWidth, mHeight);

        GL11.glShadeModel(GL11.GL_SMOOTH);
        GL11.glClearColor(0.0f, 0.7f, 0.0f, 0.5f); // Black Background
        GL11.glClearDepth(1.0f); // Depth Buffer Setup
        // GL11.glEnable(GL11.GL_DEPTH_TEST); // Enables Depth Testing
        GL11.glDepthFunc(GL11.GL_LEQUAL); // The Type Of Depth Testing To Do
        GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST); // Really
        // Nice
        // Perspective
        // Calculations
        GL11.glEnable(GL11.GL_TEXTURE_2D);

        // Transparency
        GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
        GL11.glEnable(GL11.GL_BLEND);
    }

    /**
     * Set's the display mode in fullscreen or window mode.
     * 
     * @param mFullscreen True for fullscreen mode
     */
    private void setFullscreen(boolean mFullscreen) {

        try {

            // Just in case it exists, destroy and reset display mode
            Display.destroy();
            // Display.resetDisplayMode();

            // Set the display mode
            try {

                int mode = -1;
                DisplayMode modes[] = Display.getAvailableDisplayModes();

                for (int i = 0; i < modes.length; i++) {

                    if (modes[i].getWidth() == width && modes[i].getHeight() == height && modes[i].getBitsPerPixel() >= depth) {

                        mode = i;
                        break;
                    }
                }

                if (mode != -1) {

                    System.out.println("Setting display mode to " + modes[mode]);
                    Display.setDisplayMode(modes[mode]);
                }

            } catch (Exception e) {

                System.err.println("Failed to create display mode due to " + e);
            }

            // Now try to create fullscreen window
            Display.create();
            Display.setTitle("Basic Game Example");

            if (mFullscreen) {

                isFullscreen = true;

            } else {

                isFullscreen = false;
            }

            Display.setFullscreen(isFullscreen);

        // Enable vsync if we can
        // Display.setVSyncEnabled(true);

        } catch (Exception e) {

            System.err.println("Failed to create OpenGL due to " + e);
            System.exit(1);
        }
    }

    public void setBackgroundColor(GLColor mColor) {

        GL11.glClearColor(mColor.getRed(), mColor.getGreen(), mColor.getBlue(),
                1.0f);
    }

    /**
     * Get the engine display size.
     * 
     * @return The engine's window size
     */
    public Vector2D getDisplayBounds() {

        return new Vector2D(Display.getDisplayMode().getWidth(), Display.getDisplayMode().getHeight());
    }

    /**
     * Returns whether the engine is in fullscreen mode or not.
     * 
     * @return Current fullscreen value
     */
    public boolean isFullscreen() {

        return isFullscreen;
    }

    /**
     * Starts the engine's rendering process.
     */
    public void start() {

        Utils.dump("Rendering started.", Utils.INFORMATION);

        try {

            while (!quitRequested) {

                Display.update();

                if (Display.isActive()) {

                    processKeyboard();
                    processMouse();
                    logic();
                    render();

                } else {

                    // The window is not in the foreground, so we can allow
                    // other stuff to run and
                    // infrequently update
                    try {

                        Thread.sleep(100);

                    } catch (InterruptedException e) {
                        // Do nothing
                    }
                    if (Display.isVisible() || Display.isDirty()) {

                        // Only bother rendering if the window is visible or
                        // dirty
                        logic();
                        render();
                    }
                }
            }
        } catch (OpenGLException e) {

            if (e.getMessage().equals("Invalid value (1281)")) {

                Utils.dump("Invalid image size (powers of 2 only).",
                        Utils.ERROR);
            }
            e.printStackTrace();

        } catch (Throwable t) {

            t.printStackTrace();

        } finally {

            destroy();
        }
    }

    /**
     * Changes the cursor image.
     * 
     * @param mImage The new image to assign to the cursor
     */
    public void setMouseCursor(GLImage mImage) {

        cursor = mImage;
    }

    /**
     * Requests the engine to stop execution.
     */
    public void requestQuit() {

        quitRequested = true;
    }

    /**
     * Destroys the engine and all associated sub-components and resets the
     * display mode.
     */
    public void destroy() {

        Keyboard.destroy();
        Display.destroy();

        try {
            // Display.resetDisplayMode();
        } catch (Exception e) {

            e.printStackTrace();
        }

        Utils.dump("Engine quit.", Utils.INFORMATION);

        child.onEngineExit();
    }

    /**
     * Private method to calculate the number of frames per second.
     */
    public void calculateFramesPerSecond() {

        fps++;
        if ((currentTime = Sys.getTime()) - lastTime > Sys.getTimerResolution()) {

            lastTime = currentTime;
            lastFps = fps;
            fps = 0;
        }
    }

    /**
     * Returns the current frames per second.
     * 
     * @return The average frames per second value
     */
    public int getFPS() {

        return lastFps;
    }

    /**
     * Checks if a given key is beeing held down.
     * 
     * @param mKeyCode The code for the key to check
     * @return True if the key is pressed
     */
    public boolean isKeyPressed(int mKeyCode) {

        return Keyboard.isKeyDown(mKeyCode);
    }

    public boolean keyPress(int mKeyCode) {

        if (keys[mKeyCode]) {

            keys[mKeyCode] = false;
            return true;
        }
        return false;
    }

    /**
     * Get the mouse's X position in the display window.
     * 
     * @return The X coordinate of the mouse
     */
    public int getMouseX() {

        return currentMousePosition.getX();
    }

    /**
     * Get the mouse's Y position in the display window.
     * 
     * @return The Y coordinate of the mouse
     */
    public int getMouseY() {

        return Display.getDisplayMode().getHeight() - currentMousePosition.getY();
    }

    /**
     * Gets the movement the mouse performed since last check.
     * 
     * @return A vector with x and y delta values
     */
    public Vector2D getMouseDelta() {

        return new Vector2D(currentMousePosition.getX() - lastMousePosition.getX(), currentMousePosition.getY() - lastMousePosition.getY());
    }

    /**
     * Checks if the mouse's left button is beeing pressed.
     * 
     * @return True if it is, false if it's not
     */
    public boolean getMouseLeftButtonDown() {

        return Mouse.isButtonDown(0);
    }

    /**
     * Checks if the mouse's right button is beeing pressed.
     * 
     * @return True if it is, false if it's not
     */
    public boolean getMouseRightButtonDown() {

        return Mouse.isButtonDown(1);
    }

    /**
     * Processes logic in between cycles
     */
    private final void logic() {

        // Update the gui engine
        GuiEngine.getInstance().update();

        // Tell the child to update it's logic
        child.logic();
    }

    /**
     * Private method to render to the display window. Calls the child's render
     * method as well.
     */
    private final void render() {

        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

        // Turn on lightning
        // GL11.glEnable(GL11.GL_LIGHTING);

        // Clear last image under mouse
        lastImageId = -1;
        lastImagePos = null;
        lastImageSize = null;

        // Let the main program render
        child.render();

        // Render square around image under mouse
        if (lastImageId != -1) {

            Engine.drawRect(lastImagePos.getX(), lastImagePos.getY(),
                    lastImageSize.getX(), lastImageSize.getY(), 1.0f,
                    GLColor.WHITE);
        }

        // Turn off lightning to draw interfaces
        // GL11.glDisable(GL11.GL_LIGHTING);

        // Ask the GUI engine to draw as well
        GuiEngine.getInstance().draw();

        // draw image in the mouse position
        if (cursor != null) {

            cursor.draw(getMouseX(), getMouseY(), 0.6f);
        }

        // Calculate the FPSs
        calculateFramesPerSecond();

        try {

            // Don't waste CPU
            Thread.sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();
        }
    }

    /**
     * Private method to process keyboard events. Child's keyboard events'
     * method is called.
     */
    private final void processKeyboard() {

        // Poll the keyboard for events
        Keyboard.poll();

        while (Keyboard.next()) {

            boolean keyPressed = Keyboard.getEventKeyState();
            int eventKey = Keyboard.getEventKey();
            char eventChar = Keyboard.getEventCharacter();

            // Send the key event to the gui engine and to the child
            if (keyPressed) {

                GuiEngine.getInstance().keyPressed(eventKey, eventChar);
                child.keyPressed(eventKey, eventChar);

            } else {

                GuiEngine.getInstance().keyReleased(eventKey);
                child.keyReleased(eventKey, eventChar);
                keys[eventKey] = true;
            }
            
            // The three boolean keys
            if (eventKey == Keyboard.KEY_LCONTROL || eventKey == Keyboard.KEY_RCONTROL) {

                controlKey = keyPressed;
                break;
            }
            if (eventKey == Keyboard.KEY_AT || eventKey == Keyboard.KEY_AX) {

                altKey = keyPressed;
                break;
            }
            if (eventKey == Keyboard.KEY_LSHIFT || eventKey == Keyboard.KEY_RSHIFT) {

                shiftKey = keyPressed;
                break;
            }

            if (keyPress(Keyboard.KEY_F7)) {

                try {

                    takeScreenshot();

                } catch (IOException e) {

                    // e.printStackTrace();
                    Utils.dump("Could not take screen shot: " + e.getMessage(),
                            Utils.ERROR);
                }
            }

            // Mouse grabbing. Not to use in production.
            if (eventKey == Keyboard.KEY_SPACE && keyPressed) {

                Mouse.setGrabbed(!Mouse.isGrabbed());
            }
        }
    }

    /**
     * Has the mouse pointer moved since last checking?
     * 
     * @return True if it moved, false otherwise
     */
    public boolean mouseMoved() {

        if (lastMousePosition.getX() != currentMousePosition.getX() || lastMousePosition.getY() != currentMousePosition.getY()) {

            return true;
        }
        return false;
    }

    /**
     * Private method to process mouse events. This is where the mouse position
     * gets updated.
     */
    private final void processMouse() {

        Mouse.poll();

        lastMousePosition.setX(currentMousePosition.getX());
        lastMousePosition.setY(currentMousePosition.getY());

        currentMousePosition.setX(currentMousePosition.getX() + Mouse.getDX());
        currentMousePosition.setY(currentMousePosition.getY() + Mouse.getDY());

        // Send the events to the gui engine
        while (Mouse.next()) {

            if (Mouse.getEventButton() != -1) {

                // It's mouse press
                if (Mouse.getEventButtonState()) {

                    GuiEngine.getInstance().mousePressed(Mouse.getEventButton());
                    child.mousePressed(Mouse.getEventButton());
                } // It's a mouse release
                else {

                    GuiEngine.getInstance().mouseReleased(
                            Mouse.getEventButton());
                    child.mouseReleased(Mouse.getEventButton());
                }
            }
        }

        // Check limits
        if (currentMousePosition.getX() < 0) {

            currentMousePosition.setX(0);
        }
        if (currentMousePosition.getY() < 0) {

            currentMousePosition.setY(0);
        }
        if (currentMousePosition.getX() > Display.getDisplayMode().getWidth()) {

            currentMousePosition.setX(Display.getDisplayMode().getWidth());
        }
        if (currentMousePosition.getY() > Display.getDisplayMode().getHeight()) {

            currentMousePosition.setY(Display.getDisplayMode().getHeight());
        }
    }

    /**
     * Is the control key beeing held down?
     * 
     * @return True if so, false otherwise
     */
    public boolean isControlKeyDown() {

        return controlKey;
    }

    /**
     * Is the alt key beeing held down?
     * 
     * @return True if so, false otherwise
     */
    public boolean isAltKeyDown() {

        return altKey;
    }

    /**
     * Is the shift key beeing held down?
     * 
     * @return True if so, false otherwise
     */
    public boolean isShiftKeyDown() {

        return shiftKey;
    }

    /**
     * Forces a window redraw
     */
    public void forceWindowUpdate() {

        Display.update();
    }

    public static void takeScreenshot() throws IOException {

        Utils.dump("Taking screenie", Utils.INFORMATION);
        // Get the width and height of the display and calculate the amount of
        // elements needed in the buffer
        int width = Display.getDisplayMode().getWidth();
        int height = Display.getDisplayMode().getHeight();
        int length = width * height * 3;

        // Create a buffer for the pixels on the screen and fill it
        ByteBuffer buffer = BufferUtils.createByteBuffer(length);
        GL11.glReadPixels(0, 0, width, height, GL11.GL_RGB,
                GL11.GL_UNSIGNED_BYTE, buffer);

        // A second buffer which will hold the above fetched pixeldata converted
        // to ARGB integers
        int[] bufferArray = new int[width * height];

        // Convert the fetched RGB bytes from the buffer to ARGB ints without
        // transparency. Additionally flip the image vertically because (0,0) is
        // the bottom left corner in OpenGL.
        int firstElement = length;
        int bufferIndex;
        int bufferArrayIndex = 0;
        int bytesPerRow = width * 3;

        for (int row = 0; row < height; row++) {

            firstElement -= bytesPerRow;
            bufferIndex = firstElement;

            for (int col = 0; col < width; col++) {

                int red = buffer.get(bufferIndex++);
                int green = buffer.get(bufferIndex++);
                int blue = buffer.get(bufferIndex++);

                bufferArray[bufferArrayIndex++] =
                        0xFF000000 | ((red & 0x000000FF) << 16) | ((green & 0x000000FF) << 8) | (blue & 0x000000FF);

            }

        }

        // Create the image and fill it with the generated data
        BufferedImage image =
                new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        image.setRGB(0, 0, width, height, bufferArray, 0, width);

        // Finally write the image to a file
        File outFile =
                new File((int) (System.currentTimeMillis() / 1000) + ".png");
        ImageIO.write(image, "png", outFile);
    }

    /**
     * Draws a dot with a specified size and color, in the display window.
     * 
     * @param mX The X position where to draw the dot
     * @param mY The Y position where to draw the dot
     * @param mSize The size of the dot (in pixels)
     * @param mColor The color to use for drawing
     */
    public static void drawDot(int mX, int mY, int mZ, float mSize,
            GLColor mColor) {

        GL11.glPointSize(mSize);

        mColor.set();
        GL11.glBegin(GL11.GL_POINTS);
        GL11.glVertex3i(mX, Display.getDisplayMode().getHeight() - mY, mZ);
        GL11.glEnd();
        mColor.reset();

        GL11.glPointSize(1);
    }

    /**
     * Draws a rectangle with a specified line size and color, in the display
     * window.
     * 
     * @param mX The X position where to draw the dot
     * @param mY The Y position where to draw the dot
     * @param mWidth The rectangle's width
     * @param mHeight The rectangle's height
     * @param mSize The size of the dot (in pixels)
     * @param mColor The color to use for drawing
     */
    public static void drawRect(int mX, int mY, int mWidth, int mHeight,
            float mSize, GLColor mColor) {

        GL11.glLineWidth(mSize);

        mColor.set();
        GL11.glBegin(GL11.GL_LINES);
        GL11.glVertex2i(mX - 1, Display.getDisplayMode().getHeight() - mY);
        GL11.glVertex2i(mX + mWidth + 1, Display.getDisplayMode().getHeight() - mY);

        GL11.glVertex2i(mX + mWidth + 1, Display.getDisplayMode().getHeight() - mY);
        GL11.glVertex2i(mX + mWidth + 1, Display.getDisplayMode().getHeight() - mY - mHeight);

        GL11.glVertex2i(mX + mWidth + 1, Display.getDisplayMode().getHeight() - mY - mHeight - 1);
        GL11.glVertex2i(mX - 1, Display.getDisplayMode().getHeight() - mY - mHeight - 1);

        GL11.glVertex2i(mX, Display.getDisplayMode().getHeight() - mY - mHeight);
        GL11.glVertex2i(mX, Display.getDisplayMode().getHeight() - mY);
        GL11.glEnd();
        mColor.reset();

        GL11.glPointSize(1);
    }

    /**
     * Draws a quad with a specified color, in the display window.
     * 
     * @param mX The X position where to draw the dot
     * @param mY The Y position where to draw the dot
     * @param mWidth The rectangle's width
     * @param mHeight The rectangle's height
     * @param mColor The color to use for drawing
     */
    public static void drawQuad(int mX, int mY, int mWidth, int mHeight,
            GLColor mColor) {

        mColor.set();
        GL11.glBegin(GL11.GL_QUADS);
        GL11.glVertex2i(mX, Display.getDisplayMode().getHeight() - mY);
        GL11.glVertex2i(mX, Display.getDisplayMode().getHeight() - mY - mHeight);
        GL11.glVertex2i(mX + mWidth, Display.getDisplayMode().getHeight() - mY - mHeight);
        GL11.glVertex2i(mX + mWidth, Display.getDisplayMode().getHeight() - mY);
        GL11.glEnd();
        mColor.reset();
    }

    public static void drawLine(int mX0, int mY0, int mX1, int mY1,
            float mSize, GLColor mColor) {

        GL11.glLineWidth(mSize);

        mColor.set();
        GL11.glBegin(GL11.GL_LINES);
        GL11.glVertex2i(mX0, Display.getDisplayMode().getHeight() - mY0);
        GL11.glVertex2i(mX1, Display.getDisplayMode().getHeight() - mY1);
        GL11.glEnd();
        mColor.reset();

        GL11.glPointSize(1);
    }
}